using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Packaging;
using System.Net.Mime;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using Bookvar.ObjectModel;
using Bookvar.ObjectModel.Collections;
using Bookvar.ServiceModel;

namespace Bookvar.Common
{
    internal class FileSystemPersister : IPersister
    {
        #region Constants

        private const string PackageRelationshipType = "http://schemas.microsoft.com/opc/2006/sample/document";

        private const string ResourceRelationshipType =
            "http://schemas.microsoft.com/opc/2006/sample/required-resource";

        private const string ContentPath = @"Content\";
        private const string DocumentPartName = "Document.xml";
        private const string PositionsPartName = "Positions.xml";
        private const string FloatingTopicstPartName = "FloatingTopics.xml";

        private const string DocumentPath = ContentPath + DocumentPartName;
        private const string FloatingTopicsPath = ContentPath + FloatingTopicstPartName;
        private const string PositionsPath = ContentPath + PositionsPartName;

        private const string ThumbnailPath = ContentPath + "Thumbnail.jpg";
        private const string NotesPath = ContentPath + @"Notes\";
        private const string AttachmentsPath = @"Attachments\";
        private const string ResourcesPath = @"Resources\";

        #endregion

        private readonly MindMap map;
        private readonly Dictionary<FileResourceType, string> filePaths;
        private string fileName;

        internal string FileName
        {
            get { return fileName; }
            set
            {
                fileName = value;
                map.FileName = fileName;
            }
        }

        private string tempFileName = String.Empty;
        private Package tempPackage;

        #region Constructors

        public FileSystemPersister() : this( new MindMap() )
        {
        }

        public FileSystemPersister( MindMap map ) : this( map, string.Empty )
        {
        }

        public FileSystemPersister( MindMap map, string fileName )
            : this( map, fileName, Path.GetTempFileName() )
        {
        }

        public FileSystemPersister( MindMap map, string fileName, string temporaryFileName )
        {
            this.map = map;
            this.fileName = fileName;

            map.FileName = fileName;

            tempFileName = temporaryFileName;

            filePaths = new Dictionary<FileResourceType, string>();

            filePaths.Add( FileResourceType.Attachment, @"Attachments\" );
            filePaths.Add( FileResourceType.Resource, @"Resources\" );
        }

        #endregion

        #region Public Methods

        [SuppressMessage( "Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes" )]
        public bool Save()
        {
            if ( !string.IsNullOrEmpty( fileName ) )
            {
                string filePath = Path.GetDirectoryName( fileName );
                string localTempFile = Path.Combine( filePath, Path.GetFileName( Path.GetTempFileName() ) );

                try
                {
                    using ( Package package = Package.Open( localTempFile, FileMode.Create ) )
                    {
                        SavePart( package, new Uri( DocumentPath, UriKind.Relative ), map.MainTopic );
                        SavePart( package, new Uri( FloatingTopicsPath, UriKind.Relative ), map.FloatingTopics );
                        SavePart( package, new Uri( PositionsPath, UriKind.Relative ), map.TopicPositions );

                        if ( map.MainTopic.Thumbnail != null )
                        {
                            SaveThumbnail( map.MainTopic.Thumbnail, package );
                        }
                        SaveNotesRecoursive( map.MainTopic, package );
                        SaveAttachmentsRecoursive( map.MainTopic, package );
                        SaveResourcesRecoursive( map.MainTopic, package );
                    }
                    if ( File.Exists( fileName ) )
                    {
                        File.Delete( fileName );
                    }
                    File.Move( localTempFile, fileName );
                }
                catch ( Exception )
                {
                    if ( File.Exists( localTempFile ) )
                    {
                        File.Delete( localTempFile );
                    }
                    return false;
                }
                return true;
            }
            return false;
        }

        public bool SaveFileResource( IFileResource resource, Stream resourceStream )
        {
            RemoveFileResource(resource);
            EnsureTempPackage();

            //TODO: use real file extension
            Uri partUriFile =
                PackUriHelper.CreatePartUri(
                    new Uri( filePaths[ resource.FileResourceType ] + resource.ID + ".dat", UriKind.Relative ) );

            PackagePart packagePartFile = tempPackage.CreatePart( partUriFile, MediaTypeNames.Application.Octet );

            StreamUtility.CopyStream( resourceStream, packagePartFile.GetStream() );

            tempPackage.CreateRelationship( packagePartFile.Uri, TargetMode.Internal, ResourceRelationshipType );

            return true;
        }

        public Stream LoadFileResource( IFileResource resource )
        {
            if ( tempPackage != null )
            {
                foreach (
                    PackageRelationship relationship in tempPackage.GetRelationshipsByType( ResourceRelationshipType ) )
                {
                    string relationshipUri = relationship.TargetUri.ToString();
                    if (relationshipUri.Contains(resource.FileResourceType.ToString()) &&
                         relationshipUri.Contains( resource.ID.ToString() ) )
                    {
                        Uri partUriDocument =
                            PackUriHelper.ResolvePartUri( new Uri( "/", UriKind.Relative ), relationship.TargetUri );
                        PackagePart part = tempPackage.GetPart( partUriDocument );
                        return part.GetStream();
                    }
                }
            }
            return null;
        }

        public Uri LoadThumbnail()
        {
            using ( Package package = Package.Open( fileName, FileMode.Open, FileAccess.Read ) )
            {
                PackagePart thumbnailPart = null;

                foreach ( PackageRelationship relationship in package.GetRelationshipsByType( ResourceRelationshipType )
                    )
                {
                    Uri partUriDocument =
                        PackUriHelper.ResolvePartUri( new Uri( "/", UriKind.Relative ), relationship.TargetUri );

                    string relationshipUri = relationship.TargetUri.ToString();

                    if ( relationshipUri.Contains( "Thumbnail" ) )
                    {
                        thumbnailPart = package.GetPart( partUriDocument );
                        break;
                    }
                }
                if ( thumbnailPart != null )
                {
                    Stream stream = thumbnailPart.GetStream();
                    string filePath = Path.GetTempFileName();

                    using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate))
                    {
                        StreamUtility.CopyStream(stream, fileStream);
                        return new Uri(filePath);
                    }
                }
            }
            return null;
        }

        public void RemoveFileResource( IFileResource resource )
        {
            if ( tempPackage != null )
            {
                foreach (
                    PackageRelationship relationship in tempPackage.GetRelationshipsByType( ResourceRelationshipType ) )
                {
                    string relationshipUri = relationship.TargetUri.ToString();
                    if ( relationshipUri.Contains( resource.FileResourceType.ToString() ) &&
                         relationshipUri.Contains( resource.ID.ToString() ) )
                    {
                        Uri partUriFile =
                            PackUriHelper.ResolvePartUri( new Uri( "/", UriKind.Relative ), relationship.TargetUri );
                        tempPackage.DeletePart( partUriFile );
                    }
                }
            }
        }

        public MindMap Load()
        {
            if ( !string.IsNullOrEmpty( fileName ) )
            {
                MainTopic topicTree;
                TopicPositionCollection topicPositions = null;
                TopicCollection floatingTopics = null;
                using ( Package package = Package.Open( fileName, FileMode.Open, FileAccess.Read ) )
                {
                    PackagePart documentPart = null;
                    PackagePart floatingTopicsPart = null;
                    PackagePart positionsPart = null;

                    foreach (
                        PackageRelationship relationship in package.GetRelationshipsByType( PackageRelationshipType ) )
                    {
                        Uri partUriDocument =
                            PackUriHelper.ResolvePartUri( new Uri( "/", UriKind.Relative ), relationship.TargetUri );

                        string relationshipUri = relationship.TargetUri.ToString();
                        if ( relationshipUri.Contains( DocumentPartName ) )
                        {
                            documentPart = package.GetPart( partUriDocument );
                        }
                        else if ( relationshipUri.Contains( PositionsPartName ) )
                        {
                            positionsPart = package.GetPart( partUriDocument );
                        }
                        else if ( relationshipUri.Contains( FloatingTopicstPartName ) )
                        {
                            floatingTopicsPart = package.GetPart( partUriDocument );
                        }
                    }

                    topicTree = LoadObject<MainTopic>( documentPart );

                    if ( positionsPart != null )
                    {
                        topicPositions = LoadObject<TopicPositionCollection>( positionsPart );
                    }
                    if ( floatingTopicsPart != null )
                    {
                        floatingTopics = LoadObject<TopicCollection>( floatingTopicsPart );
                    }

                    if ( topicTree != null )
                    {
                        Dictionary<string, PackagePart> noteParts = new Dictionary<string, PackagePart>();
                        Dictionary<string, PackagePart> attachmentsParts = new Dictionary<string, PackagePart>();
                        Dictionary<string, PackagePart> resourcesParts = new Dictionary<string, PackagePart>();
                        PackagePart thumbnailPart = null;
                        foreach (
                            PackageRelationship relationship in
                                package.GetRelationshipsByType( ResourceRelationshipType ) )
                        {
                            string relationshipUri = relationship.TargetUri.ToString();

                            Uri partUriDocument =
                                PackUriHelper.ResolvePartUri( new Uri( "/", UriKind.Relative ), relationship.TargetUri );

                            string partName =
                                relationshipUri.Substring( relationshipUri.LastIndexOf( "/", StringComparison.OrdinalIgnoreCase ) + 1,
                                                           relationshipUri.LastIndexOf( ".", StringComparison.OrdinalIgnoreCase ) -
                                                           relationshipUri.LastIndexOf( "/", StringComparison.OrdinalIgnoreCase ) - 1 );
                            if ( relationshipUri.Contains( "/Notes/" ) )
                            {
                                noteParts.Add( partName, package.GetPart( partUriDocument ) );
                            }
                            else if ( relationshipUri.Contains( "Attachments" ) )
                            {
                                attachmentsParts.Add( partName, package.GetPart( partUriDocument ) );
                            }
                            else if ( relationshipUri.Contains( "Resources" ) )
                            {
                                resourcesParts.Add( partName, package.GetPart( partUriDocument ) );
                            }
                            else if ( relationshipUri.Contains( "Thumbnail" ) )
                            {
                                thumbnailPart = package.GetPart( partUriDocument );
                            }
                        }
                        ClearStore();
                        if ( thumbnailPart != null )
                        {
                            Stream stream = thumbnailPart.GetStream();
                            string filePath = Path.GetTempFileName();

                            using (FileStream fileStream = new FileStream(filePath, FileMode.OpenOrCreate))
                            {
                                StreamUtility.CopyStream(stream, fileStream);
                            }

                            topicTree.Thumbnail = filePath;
                        }
                        LoadNotesRecoursive( topicTree, noteParts );
                        LoadAttachmentsRecoursive( topicTree, attachmentsParts );
                        LoadResourcesRecoursive( topicTree, resourcesParts );
                    }
                }
                map.MainTopic = topicTree;
                map.TopicPositions = topicPositions;
                map.FloatingTopics = floatingTopics;
                return map;
            }
            return null;
        }

        public void ClearStore()
        {
            if ( tempPackage != null )
            {
                tempPackage.Close();
                File.Delete( tempFileName );
            }
            tempFileName = string.Empty;
            tempPackage = null;
        }

        #endregion

        #region Private Methods

        private static void SavePart( Package package, Uri partUri, object partGraph )
        {
            Uri partUriDocument = PackUriHelper.CreatePartUri( partUri );

            PackagePart packagePartDocument = package.CreatePart( partUriDocument, MediaTypeNames.Text.Xml );

            string serialized = Serializer.Serialize( partGraph );
            using ( Stream stringStream = new MemoryStream( Encoding.UTF8.GetBytes( serialized ) ) )
            {
                StreamUtility.CopyStream( stringStream, packagePartDocument.GetStream() );
            }

            package.CreateRelationship( packagePartDocument.Uri, TargetMode.Internal, PackageRelationshipType );
        }

        private static T LoadObject<T>( PackagePart part ) where T : class
        {
            byte[] bytes;
            using ( MemoryStream documentStream = new MemoryStream() )
            {
                StreamUtility.CopyStream( part.GetStream(), documentStream );
                bytes = documentStream.ToArray();
            }

            string serilizedTopicTree = Encoding.UTF8.GetString( bytes );
            return Serializer.Deserialize<T>( serilizedTopicTree );
        }

        private static void SaveThumbnail( string thumbnailSource, Package package )
        {
            if ( !String.IsNullOrEmpty( thumbnailSource ) )
            {
                Uri partUriThumbnail = PackUriHelper.CreatePartUri( new Uri( ThumbnailPath, UriKind.Relative ) );
                PackagePart packagePartThumbnail = package.CreatePart( partUriThumbnail, MediaTypeNames.Image.Jpeg );
                using ( FileStream fileStream = new FileStream( thumbnailSource, FileMode.Open ) )
                {
                    StreamUtility.CopyStream( fileStream, packagePartThumbnail.GetStream() );
                }
                package.CreateRelationship( packagePartThumbnail.Uri, TargetMode.Internal, ResourceRelationshipType );
            }
        }

        private void SaveResourcesRecoursive( Topic topic, Package package )
        {
            foreach ( Resource resource in topic.Resources )
            {
                Uri partUriResource = PackUriHelper.CreatePartUri(
                    new Uri( ResourcesPath + topic.ID + @"\" + resource.ID + ".dat", UriKind.Relative ) );
                PackagePart packagePartResource =
                    package.CreatePart( partUriResource, MediaTypeNames.Application.Octet );

                Stream packageStream = LoadFileResource( resource );

                StreamUtility.CopyStream( packageStream, packagePartResource.GetStream() );


                package.CreateRelationship( packagePartResource.Uri,
                                            TargetMode.Internal,
                                            ResourceRelationshipType );
            }
            foreach ( Topic subTopic in topic.SubTopics )
            {
                SaveResourcesRecoursive( subTopic, package );
            }
        }

        private void SaveAttachmentsRecoursive( Topic topic, Package package )
        {
            foreach ( Attachment attachment in topic.Attachments )
            {
                Uri partUriAttachment = PackUriHelper.CreatePartUri(
                    new Uri( AttachmentsPath + topic.ID + @"\" + attachment.ID + ".dat", UriKind.Relative ) );
                PackagePart packagePartAttachment =
                    package.CreatePart( partUriAttachment, MediaTypeNames.Application.Octet );

                Stream packageStream = LoadFileResource( attachment );

                StreamUtility.CopyStream( packageStream, packagePartAttachment.GetStream() );

                package.CreateRelationship( packagePartAttachment.Uri, TargetMode.Internal, ResourceRelationshipType );
            }
            foreach ( Topic subTopic in topic.SubTopics )
            {
                SaveAttachmentsRecoursive( subTopic, package );
            }
        }

        private static void SaveNotesRecoursive( Topic topic, Package package )
        {
            if ( topic.Note != null )
            {
                Uri partUriNotes = PackUriHelper.CreatePartUri(
                    new Uri( NotesPath + topic.ID + ".xaml", UriKind.Relative ) );
                PackagePart packagePartNotes =
                    package.CreatePart( partUriNotes,
                                        MediaTypeNames.Application.Zip );

                using ( Stream packageStream = new MemoryStream() )
                {
                    topic.Note.Save( packageStream, DataFormats.XamlPackage );

                    StreamUtility.CopyStream( packageStream, packagePartNotes.GetStream() );

                    package.CreateRelationship( packagePartNotes.Uri, TargetMode.Internal, ResourceRelationshipType );
                }
            }
            foreach ( Topic subTopic in topic.SubTopics )
            {
                SaveNotesRecoursive( subTopic, package );
            }
        }

        private static void LoadNotesRecoursive( Topic topic, IDictionary<string, PackagePart> noteParts )
        {
            if ( noteParts.ContainsKey( topic.ID.ToString() ) )
            {
                PackagePart notePart = noteParts[ topic.ID.ToString() ];
                MemoryStream noteStream = new MemoryStream();

                StreamUtility.CopyStream( notePart.GetStream(), noteStream );


                FlowDocument doc = new FlowDocument();
                TextRange range = new TextRange( doc.ContentStart, doc.ContentEnd );
                range.Load( noteStream, DataFormats.XamlPackage );
                topic.Note = range;
            }

            foreach ( Topic subTopic in topic.SubTopics )
            {
                LoadNotesRecoursive( subTopic, noteParts );
            }
        }

        private void LoadAttachmentsRecoursive( Topic topic, IDictionary<string, PackagePart> attachmentsParts )
        {
            foreach ( Attachment attachment in topic.Attachments )
            {
                if ( attachmentsParts.ContainsKey( attachment.ID.ToString() ) )
                {
                    PackagePart attachmentPart = attachmentsParts[ attachment.ID.ToString() ];
                    SaveFileResource( attachment, attachmentPart.GetStream() );
                }
            }
            foreach ( Topic subTopic in topic.SubTopics )
            {
                LoadAttachmentsRecoursive( subTopic, attachmentsParts );
            }
        }

        private void LoadResourcesRecoursive( Topic topic, IDictionary<string, PackagePart> resourcesParts )
        {
            if ( topic.Resources != null )
            {
                foreach ( Resource resource in topic.Resources )
                {
                    if ( resourcesParts.ContainsKey( resource.ID.ToString() ) )
                    {
                        PackagePart resourcePart = resourcesParts[ resource.ID.ToString() ];
                        SaveFileResource( resource, resourcePart.GetStream() );
                    }
                }
            }
            foreach ( Topic subTopic in topic.SubTopics )
            {
                LoadResourcesRecoursive( subTopic, resourcesParts );
            }
        }

        private void EnsureTempFileName()
        {
            if ( String.IsNullOrEmpty( tempFileName ) )
            {
                tempFileName = Path.GetTempFileName();
            }
        }

        private void EnsureTempPackage()
        {
            EnsureTempFileName();
            if ( tempPackage == null )
            {
                tempPackage = Package.Open( tempFileName, FileMode.Create );
            }
        }

        #endregion
    }
}